/* 
 * LockManager.java 29.06.2011
 *  
 *  
 * Candidate: Alessandra Otterbach
 * Prometric ID: Algisina1
 * Username: Algisina
 *  
 * Sun Certified Developer for Java 2 Platform, Standard Edition Programming 
 * Assignment (CX-310-252A)
 *  
 * This class is part of the Programming Assignment of the Sun Certified 
 * Developer for Java 2 Platform, Standard Edition certification program, must 
 * not be used out of this context and must be used exclusively by Sun 
 * Microsystems, Inc. 
 */
package suncertify.db;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Class to do the locking mechanisms for a record.
 * 
 * @author Alessandra Otterbach
 * 
 */
public class LockManager {

    private Map<Integer, Long> lockedRecords = new HashMap<Integer, Long>();

    /** Length of time that will be waited until a timeout occurs */
    private static final int TIMEOUT = 2000;
    
    private static Lock lock = new ReentrantLock();

    private static Condition lockReleased = lock.newCondition();

    /**
     * Locks a record so that it can only be updated or deleted by this client.
     * Returned value is a cookie that must be used when the record is unlocked,
     * updated, or deleted. If the specified record is already locked by a
     * different client, the current thread gives up the CPU and consumes no CPU
     * cycles until the record is unlocked.
     * 
     * @param recNo
     *            number of the record to be locked
     * @return lockCookie
     * @throws RecordNotFoundException
     */
    public long lock(int recNo) throws RecordNotFoundException, DataException {
	lock.lock();
	try {
	    // Find out if record already is locked
	    long endTimeMSec = System.currentTimeMillis() + TIMEOUT;
	    while (lockedRecords.containsKey(recNo)) {
		long timeLeftMSec = endTimeMSec - System.currentTimeMillis();

		if (!lockReleased.await(timeLeftMSec, TimeUnit.MILLISECONDS)) {
		    throw new DataException(
			    "Timeout while waiting for a record. Please refresh and try again.");
		}
	    }
	} catch (InterruptedException e) {
	    throw new DataException(
		    "Error during locking the record. Please refresh and try again.");
	} finally {
	    // lock must always be released
	    lock.unlock();
	}
	long cookieValue = System.currentTimeMillis();
	lockedRecords.put(recNo, cookieValue);

	return cookieValue;
    }

    /**
     * Releases the lock on a record. Cookie must be the cookie returned when
     * the record was locked; otherwise throws SecurityException.
     * 
     * @param recNo
     *            number of the record to be unlocked
     * @param cookie
     *            cookie, this record should be locked with
     * @throws RecordNotFoundException
     * @throws SecurityException
     */
    public void unlock(int recNo, long cookie) throws RecordNotFoundException,
	    SecurityException {
	// Check if record is locked at all and with the given cookie, if not,
	// SecExc
	// Cookie must be the cookie returned when the record was locked
	lock.lock();
	try {
	    // Check if record is locked at all and with given
	    // lockCookie
	    if (checkLocked(recNo, cookie)) {
		// Remove lock
		lockedRecords.remove(recNo);
		lockReleased.signalAll();
	    } else {
		throw new SecurityException();
	    }
	} finally {
	 // lock must always be released
	    lock.unlock();
	}

    }

    /**
     * Check if record is locked at all (!= null) and with given lockCookie
     * 
     * @return true if record is locked
     */
    public boolean checkLocked(int recNo, long lockCookie) {

	// Check if record is locked at all and with given lockCookie
	if (lockedRecords.get(recNo) == null
		|| lockCookie != lockedRecords.get(recNo)) {
	    return false;
	}
	return true;
    }

}
